

package net.qiqbframework.spring.aggregate;


import net.qiqbframework.config.Configuration;
import net.qiqbframework.modelling.domain.*;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.annotation.Nonnull;
import java.util.Map;

import static java.lang.String.format;

public class SpringPrototypeAggregateFactory<T>
        implements AggregateFactory<T>, InitializingBean, ApplicationContextAware, BeanNameAware {

    private final Class<T> aggregateType;
    private final String prototypeBeanName;
    private final Map<Class<? extends T>, String> subtypes;

    private ApplicationContext applicationContext;
    private String beanName;
    private AggregateFactory<T> delegate;


    public SpringPrototypeAggregateFactory(Class<T> aggregateType,
                                           String prototypeBeanName,
                                           Map<Class<? extends T>, String> subtypes) {
        this.aggregateType = aggregateType;
        this.prototypeBeanName = prototypeBeanName;
        this.subtypes = subtypes;
    }


    @SuppressWarnings("unused")
    public static <T> SpringPrototypeAggregateFactory<T> withSubtypeSupport(
            Class<T> aggregateType,
            String prototypeBeanName,
            Map<Class<? extends T>, String> subtypes
    ) {
        return new SpringPrototypeAggregateFactory<>(aggregateType, prototypeBeanName, subtypes);
    }

    @Override
    public T createAggregateRoot(String aggregateIdentifier) {
        return delegate.createAggregateRoot(aggregateIdentifier);
    }

    @Override
    public Class<T> getAggregateType() {
        return aggregateType;
    }

    @Override
    public void setApplicationContext(@Nonnull ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setBeanName(@Nonnull String beanName) {
        this.beanName = beanName;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void afterPropertiesSet() {
        // Only when there are no subtypes can we be certain the set prototypeBeanName is an existing bean definition.
        // This stems from the intent that the root of a polymorphic aggregate is abstract, leading to no bean definition.
        if (subtypes.isEmpty() && !applicationContext.isPrototype(prototypeBeanName)) {
            throw new IncompatibleAggregateException(format(
                    "Cannot initialize repository '%s'. "
                            + "The bean with name '%s' does not have the 'prototype' scope.",
                    beanName, prototypeBeanName
            ));
        }

        AggregateModel<T> model;
        if (applicationContext.getBeanNamesForType(Configuration.class).length > 0) {
            Configuration configuration = applicationContext.getBean(Configuration.class);
            model = AnnotatedAggregateModelFactory.inspectAggregate(getAggregateType(),
                    configuration.parameterResolverFactory(),
                    configuration.handlerDefinition(getAggregateType()),
                    subtypes.keySet(), null);
        } else {
            model = AnnotatedAggregateModelFactory.inspectAggregate(getAggregateType(),
                    subtypes.keySet(), null);
        }
        this.delegate = new AbstractAggregateFactory<T>(model) {
            @Override
            protected T doCreateAggregate(String aggregateIdentifier) {
                return (T) applicationContext.getBean(prototype());
            }

            private String prototype() {
                return aggregateModel().type(aggregateType.getTypeName())
                        .map(subtypes::get)

                        .orElse(prototypeBeanName);
            }

            @Override
            protected T postProcessInstance(T aggregate) {
                applicationContext.getAutowireCapableBeanFactory()
                        .configureBean(aggregate, beanNameFor(aggregate.getClass()));
                return aggregate;
            }

            private String beanNameFor(Class<?> aggregateClass) {
                return aggregateClass != aggregateType ? subtypes.get(aggregateClass) : prototypeBeanName;
            }
        };
    }
}
